﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.Xml;
using Notoric.Model.Internal;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Describes a color in terms of alpha, red, green and blue channels.
    /// </summary>
    /// <remarks>
    /// The structure fields are not backed up by an XML element, but the structure
    /// has overriden the <see cref="ToString"/> method and can be parsed from a <see cref="System.String"/>.
    /// </remarks>
    public struct Color
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Color"/> instance and initializes its channels.
        /// </summary>
        /// <param name="r">Value of the red channel</param>
        /// <param name="g">Value of the green channel</param>
        /// <param name="b">Value of the blue channel</param>
        /// <param name="a">Value of the alpha channel</param>
        public Color(byte r, byte g, byte b, byte a)
            : this()
        {
            Red = r;
            Green = g;
            Blue = b;
            Alpha = a;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Returns a deep copy of this color structure.
        /// </summary>
        /// <returns>A new <see cref="Color"/> instance that is a deep copy of this structure</returns>
        public Color Clone()
        {
            return new Color(Red, Green, Blue, Alpha);
        }

        /// <summary>
        /// Converts a string representation of the color to the color structure.
        /// </summary>
        /// <param name="s">A string containing the color to convert</param>
        /// <returns>The parsed <see cref="Color"/> instance</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="s"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="FormatException">
        /// <paramref name="s"/> is not in the correct format.
        /// </exception>
        public static Color Parse(string s)
        {
            if (s == null)
                throw new ArgumentNullException("s");
            if (s.Length != 8)
                throw new FormatException("The color string is not in the correct format");

            string alphaS = s.Substring(0, 2);
            string redS = s.Substring(2, 2);
            string greenS = s.Substring(4, 2);
            string blueS = s.Substring(6, 2);

            Color color = new Color(
                Byte.Parse(redS, NumberStyles.HexNumber),
                Byte.Parse(greenS, NumberStyles.HexNumber),
                Byte.Parse(blueS, NumberStyles.HexNumber),
                Byte.Parse(alphaS, NumberStyles.HexNumber)
                );

            return color;
        }

        /// <inheritdoc/>
        public override string ToString()
        {
            return Alpha.ToString("X2") + Red.ToString("X2") + Green.ToString("X2") + Blue.ToString("X2");
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the value of the red channel.
        /// </summary>
        public byte Red { get; set; }
        /// <summary>
        /// Gets or sets the value of the green channel.
        /// </summary>
        public byte Green { get; set; }
        /// <summary>
        /// Gets or sets the value of the blue channel.
        /// </summary>
        public byte Blue { get; set; }
        /// <summary>
        /// Gets or sets the value of the alpha channel.
        /// </summary>
        public byte Alpha { get; set; }

        #endregion
    }

    /// <summary>
    /// Represents a point in a 2-dimensional real space.
    /// </summary>
    /// <remarks>
    /// The structure fields are backed up by an XML element.
    /// However, to use it, the <see cref="ModelElement"/> has to call
    /// <see cref="SetOwner"/> method first.
    /// </remarks>
    public struct Point : INotifyPropertyChanged
    {
        #region Constructors

        /// <summary>
        /// Initializes a new <see cref="Point"/> instance with given values.
        /// </summary>
        /// <param name="x">
        /// <para>
        /// Type: <see cref="System.Double"/>
        /// </para>
        /// <para>
        /// The horizontal (width) coordinate of the point.
        /// </para>
        /// </param>
        /// <param name="y">
        /// <para>
        /// The vertical (height) coordinate of the point.
        /// </para>
        /// </param>
        public Point(double x, double y)
            : this()
        {
            this.x = x;
            this.y = y;
        }

        /// <summary>
        /// Initializes a new <see cref="Point"/> instance and loads its fields from a given XML element.
        /// </summary>
        /// <param name="element">
        /// <para>
        /// Type: <see cref=" XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that contains the point data.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="element"/> is <see langword="null"/>.
        /// </exception>
        public Point(XmlElement element)
            : this()
        {
            if (element == null)
                throw new ArgumentNullException("element");

            this.element = element;
            this.owner = (XmlElement)element.ParentNode;
            this.xAttribute = element.Attributes[PackageVocabulary.XAttribute];
            this.yAttribute = element.Attributes[PackageVocabulary.YAttribute];

            x = double.Parse(xAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            y = double.Parse(yAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Point"/> struct.
        /// </summary>
        /// <param name="source">
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The source point.
        /// </para>
        /// </param>
        public Point(Point source)
            : this()
        {
            this.x = source.x;
            this.y = source.y;
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when the value of some property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Internal Methods

        /// <summary>
        /// Sets the XML element that directly owns the XML element that
        /// backups this point.
        /// If the point previously had an owner, this method removes the element from it.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that directly owns the element of this point.
        /// </para>
        /// </param>
        internal void SetOwner(XmlElement owner)
        {
            if (owner != this.owner)
            {
                if (this.owner != null)                    
                    this.owner.RemoveChild(element);

                element = null;
                xAttribute = null;
                yAttribute = null;
                
                this.owner = owner;

                if (owner != null)
                {
                    element = owner.OwnerDocument.CreateElement(
                        PackageVocabulary.PackageNamespacePrefix,
                        PackageVocabulary.PointElementName,
                        PackageVocabulary.PackageNamespaceUri
                        );

                    xAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.XAttribute);
                    yAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.YAttribute);

                    xAttribute.Value = x.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    yAttribute.Value = y.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                    element.Attributes.Append(xAttribute);
                    element.Attributes.Append(yAttribute);
                    owner.AppendChild(element);
                }
            }
        }

        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets the XML element that backups the data of this point.
        /// </summary>
        internal XmlElement Element
        {
            get { return element; }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the horizontal coordinate, in centimeters, of the point.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the horizontal offset, in centimeters, of the point from the origin of coordinates.
        /// The default is 0.0.
        /// </value>
        public double X
        {
            get { return x; }
            set
            {
                x = value;
                if (xAttribute != null)
                    xAttribute.Value = x.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("X"));
            }
        }

        /// <summary>
        /// Gets or sets the vertical coordinate, in centimeters, of the point.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the vertical offset, in centimeters, of the point from the origin of coordinates.
        /// The default is 0.0.
        /// </value>
        public double Y
        {
            get { return y; }
            set
            {
                y = value;
                if (yAttribute != null)
                    yAttribute.Value = y.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Y"));
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the XML element that backups this point.
        /// </summary>
        private XmlElement element;
        /// <summary>
        /// Holds a reference to the <see cref="XmlElement"/> that directly owns this point.
        /// </summary>
        private XmlElement owner;
        /// <summary>
        /// Holds the value of the <see cref="X"/> field.
        /// </summary>
        private double x;
        /// <summary>
        /// Holds the XML attribute that backups the value of the <see cref="X"/> field.
        /// </summary>
        private XmlAttribute xAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Y"/> field.
        /// </summary>
        private double y;
        /// <summary>
        /// Holds the XML attrubute that backups the value of the <see cref="Y"/> field.
        /// </summary>
        private XmlAttribute yAttribute;

        #endregion
    }

    /// <summary>
    /// Represents the dimensions of an object in a 2-dimensional real space.
    /// </summary>
    /// <remarks>
    /// The structure fields are backed up by an XML element.
    /// However, to use it, the <see cref="ModelElement"/> has to call
    /// <see cref="SetOwner"/> method first.
    /// </remarks>
    public struct Size : INotifyPropertyChanged
    {
        #region Constructors

        /// <summary>
        /// Initializes a new <see cref="Size"/> instance with given values.
        /// </summary>
        /// <param name="width">
        /// <para>
        /// Type: <see cref="System.Double"/>
        /// </para>
        /// <para>
        /// The width of the object.
        /// </para>
        /// </param>
        /// <param name="height">
        /// <para>
        /// Type: <see cref="System.Double"/>
        /// </para>
        /// <para>
        /// The height of the object.
        /// </para>
        /// </param>
        public Size(double width, double height)
            : this()
        {
            this.width = width;
            this.height = height;
        }

        /// <summary>
        /// Initializes a new <see cref="Size"/> instance and loads it from a given XML element.
        /// </summary>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that contains the data of this size.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="element"/> is <see langword="null"/>.
        /// </exception>
        public Size(XmlElement element)
            : this()
        {
            if (element == null)
                throw new ArgumentNullException("element");

            this.element = element;
            this.owner = (XmlElement)element.ParentNode;
            widthAttribute = element.Attributes[PackageVocabulary.WidthAttribute];
            heightAttribute = element.Attributes[PackageVocabulary.HeightAttribute];

            width = double.Parse(widthAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            height = double.Parse(heightAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when the value of some property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Internal Methods

        /// <summary>
        /// Sets the XML element that directly owns the XML element that
        /// backups this size.
        /// If the size previously had an owner, this method removes the element from it.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that directly owns the element of this size.
        /// </para>
        /// </param>
        internal void SetOwner(XmlElement owner)
        {
            if (owner != this.owner)
            {
                if (this.owner != null)
                    this.owner.RemoveChild(element);

                element = null;
                widthAttribute = null;
                heightAttribute = null;

                this.owner = owner;

                if (owner != null)
                {
                    element = owner.OwnerDocument.CreateElement(
                        PackageVocabulary.PackageNamespacePrefix,
                        PackageVocabulary.SizeElementName,
                        PackageVocabulary.PackageNamespaceUri
                        );

                    widthAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.WidthAttribute);
                    heightAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.HeightAttribute);

                    widthAttribute.Value = width.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    heightAttribute.Value = height.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                    element.Attributes.Append(widthAttribute);
                    element.Attributes.Append(heightAttribute);
                    owner.AppendChild(element);
                }
            }
        }

        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets the XML element that backups the data of this size.
        /// </summary>
        internal XmlElement Element
        {
            get { return element; }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the height, in centimeters, of the object.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the height, in centimeters, of the object.
        /// The default is 0.0.
        /// </value>
        public double Width
        {
            get { return width; }
            set
            {
                width = value;
                if (widthAttribute != null)
                    widthAttribute.Value = width.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Width"));
            }
        }

        /// <summary>
        /// Gets or sets the width, in centimeters, of the object.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the width, in centimeters, of the object.
        /// The default is 0.0.
        /// </value>
        public double Height
        {
            get { return height; }
            set
            {
                height = value;
                if (heightAttribute != null)
                    heightAttribute.Value = height.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Height"));
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the XML element that backups this size.
        /// </summary>
        private XmlElement element;
        /// <summary>
        /// Holds a reference to the <see cref="XmlElement"/> that directly owns this size.
        /// </summary>
        private XmlElement owner;
        /// <summary>
        /// Holds the value of the <see cref="Width"/> field.
        /// </summary>
        private double width;
        /// <summary>
        /// Holds the XML attribute that backups the value of the <see cref="Width"/> field.
        /// </summary>
        private XmlAttribute widthAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Height"/> field.
        /// </summary>
        private double height;
        /// <summary>
        /// Holds the XML attrubute that backups the value of the <see cref="Height"/> field.
        /// </summary>
        private XmlAttribute heightAttribute;

        #endregion
    }

    /// <summary>
    /// Describes the thickness of a frame around a rectangle.
    /// </summary>
    /// <remarks>
    /// The structure fields are backed up by an XML element.
    /// However, to use it, the <see cref="ModelElement"/> has to call
    /// <see cref="SetOwner"/> method first.
    /// </remarks>
    public struct Thickness : INotifyPropertyChanged
    {
        #region Constructors

        /// <summary>
        /// Initializes a new <see cref="Thickness"/> instance with given values.
        /// </summary>
        /// <param name="left">Value of the <see cref="Left"/> property</param>
        /// <param name="top">Value of the <see cref="Top"/> property</param>
        /// <param name="right">Value of the <see cref="Right"/> property</param>
        /// <param name="bottom">Value of the <see cref="Bottom"/> property</param>
        public Thickness(double left, double top, double right, double bottom)
            : this()
        {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }

        /// <summary>
        /// Initializes a new <see cref="Thickness"/> instance and loads it from a given XML element.
        /// </summary>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that contains the data of this thickness.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="element"/> is <see langword="null"/>.
        /// </exception>
        public Thickness(XmlElement element)
            : this()
        {
            if (element == null)
                throw new ArgumentNullException("element");

            this.element = element;
            this.owner = (XmlElement)element.ParentNode;
            bottomAttribute = element.Attributes[PackageVocabulary.BottomAttribute];
            leftAttribute = element.Attributes[PackageVocabulary.LeftAttribute];
            topAttribute = element.Attributes[PackageVocabulary.TopAttribute];
            rightAttribute = element.Attributes[PackageVocabulary.RightAttribute];

            bottom = double.Parse(bottomAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            left = double.Parse(leftAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            top = double.Parse(topAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            right = double.Parse(rightAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when the value of some property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Internal Methods

        /// <summary>
        /// Sets the XML element that directly owns the XML element that
        /// backups this size.
        /// If the size previously had an owner, this method removes the element from it.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that directly owns the element of this size.
        /// </para>
        /// </param>
        internal void SetOwner(XmlElement owner)
        {
            if (owner != this.owner)
            {
                if (this.owner != null)
                    this.owner.RemoveChild(element);

                element = null;
                bottomAttribute = null;
                leftAttribute = null;
                topAttribute = null;
                rightAttribute = null;

                this.owner = owner;

                if (owner != null)
                {
                    element = owner.OwnerDocument.CreateElement(
                        PackageVocabulary.PackageNamespacePrefix,
                        PackageVocabulary.ThicknessElementName,
                        PackageVocabulary.PackageNamespaceUri
                        );

                    bottomAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.BottomAttribute);
                    leftAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.LeftAttribute);
                    topAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.TopAttribute);
                    rightAttribute = owner.OwnerDocument.CreateAttribute(PackageVocabulary.RightAttribute);

                    bottomAttribute.Value = bottom.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    leftAttribute.Value = left.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    topAttribute.Value = top.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    rightAttribute.Value = right.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                    element.Attributes.Append(bottomAttribute);
                    element.Attributes.Append(leftAttribute);
                    element.Attributes.Append(topAttribute);
                    element.Attributes.Append(rightAttribute);
                    owner.AppendChild(element);
                }
            }
        }

        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets the XML element that backups the data of this thickness.
        /// </summary>
        internal XmlElement Element
        {
            get { return element; }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the width, in centimeters, of the bottom side of the bounding rectangle.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the width, in centimeters, of the bottom side of the bounding rectangle. 
        /// The default is 0.0.
        /// </value>
        public double Bottom
        {
            get { return bottom; }
            set
            {
                bottom = value;
                if (bottomAttribute != null)
                    bottomAttribute.Value = bottom.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Bottom"));
            }
        }

        /// <summary>
        /// Gets or sets the width, in centimeters, of the left side of the bounding rectangle.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the width, in centimeters, of the left side of the bounding rectangle. 
        /// The default is 0.0.
        /// </value>
        public double Left
        {
            get { return left; }
            set
            {
                left = value;
                if (leftAttribute != null)
                    leftAttribute.Value = left.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Left"));
            }
        }

        /// <summary>
        /// Gets or sets the width, in centimeters, of the upper side of the bounding rectangle.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the width, in centimeters, of the upper side of the bounding rectangle. 
        /// The default is 0.0.
        /// </value>
        public double Top
        {
            get { return top; }
            set
            {
                top = value;
                if (topAttribute != null)
                    topAttribute.Value = top.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Top"));
            }
        }

        /// <summary>
        /// Gets or sets the width, in centimeters, of the right side of the bounding rectangle.
        /// </summary>
        /// <value>
        /// A <see cref="double"/> that represents the width, in centimeters, of the right side of the bounding rectangle. 
        /// The default is 0.0.
        /// </value>
        public double Right
        {
            get { return right; }
            set
            {
                right = value;
                if (rightAttribute != null)
                    rightAttribute.Value = right.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Right"));
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Bottom"/> field.
        /// </summary>
        private double bottom;
        /// <summary>
        /// Holds the XML attribute that backups the value of the <see cref="Bottom"/> field.
        /// </summary>
        private XmlAttribute bottomAttribute;
        /// <summary>
        /// Holds the XML element that backups this thickness.
        /// </summary>
        private XmlElement element;
        /// <summary>
        /// Holds a reference to the <see cref="XmlElement"/> that directly owns this thickness.
        /// </summary>
        private XmlElement owner;
        /// <summary>
        /// Holds the value of the <see cref="Left"/> field.
        /// </summary>
        private double left;
        /// <summary>
        /// Holds the XML attribute that backups the value of the <see cref="Left"/> field.
        /// </summary>
        private XmlAttribute leftAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Right"/> field.
        /// </summary>
        private double right;
        /// <summary>
        /// Holds the XML attribute that backups the value of the <see cref="Right"/> field.
        /// </summary>
        private XmlAttribute rightAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Top"/> field.
        /// </summary>
        private double top;
        /// <summary>
        /// Holds the XML attrubute that backups the value of the <see cref="Top"/> field.
        /// </summary>
        private XmlAttribute topAttribute;

        #endregion
    }

}